﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Opennav.MapGenerator.Geometry;
using System.IO;
using System.Diagnostics;

namespace Opennav.MapGenerator.Map.MapTiles
{
    public class MapTile
    {
        private Zoom zoom = null;
        private Area initialBounds = null;
        private Area fullBounds = new Area();
        private int visibleObjects = 0;
        private List<MapLine> lines = new List<MapLine>();
        private List<MapShape> shapes = new List<MapShape>();
        private List<MapPoint> points = new List<MapPoint>();

        private MapTile(Zoom zoom)
        {
            this.zoom = zoom;
            this.Last = false;
        }

        private MapTile(Area initialBounds, Zoom zoom)
        {
            this.initialBounds = initialBounds;
            this.zoom = zoom;
            this.Last = false;
        }

        public Zoom Zoom
        {
            get { return zoom; }
        }

        public int VisibleObjectCount
        {
            get { return visibleObjects; }
        }

        public int ObjectCount
        {
            get { return points.Count + lines.Count + shapes.Count; }
        }

        public Area FullBounds
        {
            get { return fullBounds; }
        }

        public Area InitialBounds
        {
            get
            {
                if (initialBounds == null)
                    return FullBounds;
                else
                    return initialBounds;
            }
        }

        public ushort Id { get; set; }

        public ushort SubtileId { get; set; }

        public bool Last { get; set; }

        public int Offset { get; set; }

        public Coordinate Center { 
            get { return FullBounds.Center; } 
        }

        public IEnumerable<MapObject> Objects 
        {
            get 
            {
                foreach (MapPoint point in points) 
                    yield return point;
                foreach (MapLine line in lines)
                    yield return line;
                foreach (MapShape shape in shapes)
                    yield return shape;
            }
        }

        public void Add(MapObject obj)
        {
            if (zoom == null || obj.MaxResolution >= zoom.Resolution)
            {
                if (obj is MapLine)
                {
                    lines.Add((MapLine)obj);
                    if (zoom != null && obj.MinResolution <= zoom.Resolution)
                    {
                        visibleObjects++;
                    }
                    fullBounds.AddRangeToBounds(((MapLine)obj).Coordinates);
                }
                else if (obj is MapShape)
                {
                    shapes.Add((MapShape)obj);
                    if (zoom != null && obj.MinResolution <= zoom.Resolution)
                    {
                        visibleObjects++;
                    }
                    fullBounds.AddRangeToBounds(((MapShape)obj).Coordinates);
                }
                else if (obj is MapPoint)
                {
                    points.Add((MapPoint)obj);
                    if (zoom != null && obj.MinResolution <= zoom.Resolution)
                    {
                        visibleObjects++;
                    }
                    fullBounds.AddToBounds(((MapPoint)obj).Coordinate);
                }
            }
        }

        #region Writer

        public uint LAST_TILE_FLAG = 0x80000000;

        public void WriteIndex(Stream stream)
        {
            BinaryWriter writer = new BinaryWriter(stream);
            writer.Write((byte)(Center.Longitude & 0xff));
            writer.Write((short)(Center.Longitude >> 8));
            writer.Write((byte)(Center.Latitude & 0xff));
            writer.Write((short)(Center.Latitude >> 8));
            writer.Write((byte)Zoom.Level);
            int width = this.FullBounds.Width >> this.Zoom.Shift;
            int height = this.FullBounds.Height >> this.Zoom.Shift;
            Debug.Assert(width <= ushort.MaxValue);
            Debug.Assert(height <= ushort.MaxValue);
            writer.Write((ushort)width);
            writer.Write((ushort)height);
            writer.Write(SubtileId);
            uint offset = (uint)Offset;
            if (Last) offset = offset | LAST_TILE_FLAG;
            writer.Write(offset);
        }

        public void WriteTile(Stream stream)
        {
            List<MapObject> objects = new List<MapObject>();
            objects.AddRange(from o in Objects where o.MinResolution <= Zoom.Resolution select o);
            for (short id = 0; id < objects.Count; id++) 
            {
                objects[id].Write(stream, this, (id == objects.Count - 1));
                objects[id].References.Add(new MapReference(this.Id, id));
            }
        }

        #endregion

        #region Static Methods

        private static int MAX_TILE_DIM = 0x7fff;
        private static int MAX_TILE_OBJECTS = 0x3ff;
        private static int MAX_OBJECT_DIM = 0x7fff / 2;

        public static IEnumerable<MapTile> Create(IEnumerable<MapObject> objects, Zoom zoomLevel)
        {
            MapTile fullMap = new MapTile(zoomLevel);
            foreach (MapObject obj in objects)
                PutObject(obj, fullMap);
            foreach (MapTile splitted in Split(fullMap))
                yield return splitted;
        }

        public static IEnumerable<MapTile> CreateLevel(MapTile tile, Zoom level)
        {
            MapTile newTile = new MapTile(level);
            foreach (MapObject obj in tile.Objects)
                PutObject(obj, newTile);
            foreach (MapTile splitted in Split(newTile))
                yield return splitted;
        }

        private static IEnumerable<MapTile> Split(MapTile tile)
        {
            foreach (MapTile dimSplittedTile in SplitDimension(tile))
            {
                foreach (MapTile sizeSplittedTile in SplitSize(dimSplittedTile))
                {
                    if (sizeSplittedTile.ObjectCount > 0)
                        yield return sizeSplittedTile;
                }
            }
        }

        private static IEnumerable<MapTile> SplitDimension(MapTile tile)
        {
            int w = (tile.InitialBounds.Width >> tile.Zoom.Shift) + 1;
            int h = (tile.InitialBounds.Height >> tile.Zoom.Shift) + 1;
            if (w <= MAX_TILE_DIM && h <= MAX_TILE_DIM)
                yield return tile;
            else
            {
                int nx = w / MAX_TILE_DIM, ny = h / MAX_TILE_DIM;
                if (w % MAX_TILE_DIM > 0) nx++;
                if (h % MAX_TILE_DIM > 0) ny++;
                List<MapTile> splittedTiles = new List<MapTile>();
                foreach (Area area in tile.InitialBounds.SplitBounds(nx, ny))
                {
                    splittedTiles.Add(new MapTile(area, tile.Zoom));
                }
                PutObjects(tile.Objects, splittedTiles);
                foreach (MapTile result in splittedTiles)
                    yield return result;
            }
        }

        private static IEnumerable<MapTile> SplitSize(MapTile tile)
        {
            if (tile.VisibleObjectCount <= MAX_TILE_OBJECTS)
            {
                yield return tile;
            }
            else
            {
                List<MapTile> splittedTiles = new List<MapTile>();
                foreach (Area area in tile.InitialBounds.SplitBounds(2, 2))
                {
                    splittedTiles.Add(new MapTile(area, tile.Zoom));
                }
                PutObjects(tile.Objects, splittedTiles);
                foreach (MapTile splittedTile in splittedTiles)
                {
                    foreach (MapTile result in SplitSize(splittedTile))
                        yield return result;
                }
            }
        }

        private static void PutObjects(IEnumerable<MapObject> objects, List<MapTile> tiles)
        {
            foreach (MapObject obj in objects)
            {
                foreach (MapTile tile in tiles)
                {
                    if (tile.InitialBounds.Contains(obj.Coordinate))
                    {
                        PutObject(obj, tile);
                        break;
                    }
                }
            }
        }

        private static void PutObject(MapObject obj, MapTile tile)
        {
            if (obj is MapPoint)
                tile.Add(obj);
            else if (obj is MapLine)
                PutLine((MapLine)obj, tile);
            else if (obj is MapShape)
                PutShape((MapShape)obj, tile);
        }

        private static void PutShape(MapShape shape, MapTile tile)
        {
            if (shape.Bounds.MaxDimension > MAX_OBJECT_DIM)
            {
                Console.WriteLine("Shape to big, splitting");
                int nx = shape.Bounds.Width / MAX_OBJECT_DIM;
                if (shape.Bounds.Width % MAX_OBJECT_DIM > 0)
                    nx++;
                int ny = shape.Bounds.Height / MAX_OBJECT_DIM;
                if (shape.Bounds.Height % MAX_OBJECT_DIM > 0)
                    ny++;
                Area[] areas = shape.Bounds.SplitBounds(nx, ny);
                foreach (Area area in areas)
                {
                    Coordinate[] clipped = MapUtils.SutherlandHodgman(shape.Coordinates, area);
                    if (clipped.Length > 2)
                    {
                        tile.Add(new MapShape(shape, clipped));
                    }
                }
            }
            else
            {
                tile.Add(shape);
            }
        }

        private static void PutLine(MapLine line, MapTile tile)
        {
            if (line.Bounds.MaxDimension > MAX_OBJECT_DIM)
            {
                //throw new Exception("Roadsegment!!!!!!");
                Console.WriteLine("Line to big, splitting");
                Area area = new Area();
                int from = 0, to = 0;
                Coordinate last = null;
                List<Coordinate> coordinates = new List<Coordinate>();
                foreach (Coordinate coord in line.Coordinates)
                {
                    area.AddToBounds(coord);
                    if (area.MaxDimension > MAX_OBJECT_DIM)
                    {
                        tile.Add(new MapLine(line, coordinates));
                        from = to;
                        area.Reset();
                        area.AddToBounds(last);
                        coordinates = new List<Coordinate>();
                        coordinates.Add(last);
                    }
                    coordinates.Add(coord);
                    last = coord;
                    to++;
                }
                if (coordinates.Count > 0)
                    tile.Add(new MapLine(line, coordinates));
            }
            else
            {
                tile.Add(line);
            }
        }

        #endregion
    }
}
